//
//  BWThemeKit Generator.swift
//  SwiftScripts
//
//  Created by zhuxuhong on 2022/3/30.
//
import Foundation

@main
enum Generator {
    static func main(){
        guard
            let projectDir = ProcessInfo.processInfo.environment["SRCROOT"]
        else {
            return
        }
        
        // theme configures
        guard let configure = loadThemeConfigure(projectDir: projectDir) else {
            return
        }
        let themeTemplate = swiftFileTemplate(forTheme: configure.name, styleEnum: .init(name: configure.name, cases: configure.styles))
        let themeSaveFile = themeSwiftFileSavePath(fileName: configure.name, projectDir: projectDir)
        var success = saveText(themeTemplate, toFile: themeSaveFile)
        print(success, themeSaveFile)
        
        // colors swift
        guard let colorFileName = configure.colors.files.first,
              let colors = loadThemeColors(fileName: colorFileName, projectDir: projectDir) else {
                  return
              }
        let subs: [Enum] = colors.map{
            Enum(name: $0.name, cases: $0.colors.map{ $0.name })
        }
        let colorEnum = Enum(name: configure.colors.name, cases: [], subs: subs)
        let colorsSaveFile = themeSwiftFileSavePath(fileName: colorEnum.name, projectDir: projectDir)
        let colorsTemplate = swiftFileTemplate(forThemeColorEnum: colorEnum)
        success = saveText(colorsTemplate, toFile: colorsSaveFile)
        print(success, colorsSaveFile)
        
        // Concat theme colors
        var styles: [ThemeColorStoreStyle] = []
        for file in configure.colors.files {
            guard let scopes = loadThemeColors(fileName: file, projectDir: projectDir) else {
                continue
            }
            styles.append(ThemeColorStoreStyle(name: file, scopes: scopes))
        }
        let store = ThemeColorStore(name: configure.colors.name, styles: styles)
        do {
            let encoder = JSONEncoder()
            encoder.outputFormatting = .prettyPrinted
            let data = try encoder.encode(store)
            let path = themeColorsSavePath(configureName: configure.colors.name, projectDir: projectDir)
            success = saveData(data, toFile: path)
            print(success, path)
        } catch {
            print(error)
        }
        
        // Update extension files
        updateSwiftExtensionFiles(themeName: configure.name, projectDir: projectDir)
    }
}

extension String {
    static let keptWords: [String] = [
        "default",
        "class"
    ]
    var convertedAvoidSwiftKeptWord: String {
        guard isSwiftKeptWord else {
            return self
        }
        return "`\(self)`"
    }
    var isSwiftKeptWord: Bool {
        return Self.keptWords.contains(self)
    }
}


enum DecodeJSONError: Error {
    case badData
}

// decoding
extension Generator {
    static func decodeJSON<T: Decodable>(
        fileAtPath path: String,
        toObjectType type: T.Type
    ) -> Result<T, Error> {
        let data = getData(fromFileAt: path)
        
        guard case .success(let data) = data else {
            return .failure(DecodeJSONError.badData)
        }
        print(data)
        
        do {
            let object = try JSONDecoder().decode(type.self, from: data)
            print(object)
            return .success(object)
        } catch {
            print("JSONDecoder().decode() Error: ", error)
            return .failure(error)
        }
    }
    
    static func getData(fromFileAt path: String) -> Result<Data, Error> {
        let aURL = URL(fileURLWithPath: path)
        print("getData :", aURL)
        do {
            let data = try Data(contentsOf: aURL)
            return .success(data)
        }
        catch {
            print("Data(contentsOf: aURL) Error: ", error)
            return .failure(error)
        }
    }
    
    static func saveData(_ data: Data, toFile atPath: String) -> Bool {
        return FileManager.default.createFile(atPath: atPath, contents: data, attributes: nil)
    }
    
    static func saveText(_ text: String, toFile atPath: String) -> Bool {
        return FileManager.default.createFile(atPath: atPath, contents: text.data(using: .utf8), attributes: nil)
    }
}

struct Enum {
    var name: String
    var cases: [String]
    var subs: [Enum]
    
    init(name: String, cases: [String], subs: [Enum] = []) {
        self.subs = subs
        self.name = name
        self.cases = cases
            .map{ $0.lowercased().convertedAvoidSwiftKeptWord }
    }
}

// file templates
extension Generator {
    static func swiftFileTemplate(forTheme fileName: String, styleEnum: Enum) -> String {
        let style = "\(fileName)Style"
        var template =
    """
    import UIKit
    import BWThemeKit
    
    /// \(fileName) Quick Adaptable
    public typealias \(fileName)AdaptableResponder = \(fileName)Adaptable & BWThemeAdaptable
    public protocol \(fileName)Adaptable {
        func updateThemeStyleIfNeeded()
    }
    public extension \(fileName)Adaptable where Self: BWThemeAdaptable {
        func updateThemeStyleIfNeeded() {
            updateThemeIfNeeded(style: \(fileName).shared.currentStyle as! Self.Style)
        }
    }
    
    /// \(fileName) Quickable Extensions
    public extension UIColor {
        typealias AppColor = \(fileName)Color
        static func use<T: BWThemeColor>(_ color: T) -> UIColor? {
            return color.themeColor
        }
    }
    
    /// \(fileName) Color Quickable
    public protocol \(fileName)ColorQuickable {
    }
    public extension BWThemeColor where Self: \(fileName)ColorQuickable {
        var themeColor: UIColor? {
            return uiColor(forTheme: \(fileName).shared.currentStyle as! Self.Style)
        }
        func uiColor(forTheme style: \(fileName)Style) -> UIColor? {
            return \(fileName)ColorStore.shared.getColor(forStyle: style, atScope: Self.self, color: self)
        }
    }
    
    /// \(fileName) Notification Name
    public extension NSNotification.Name {
        static let \(style)DidChange = NSNotification.Name(rawValue: "\(style)DidChange")
    }
    
    /// \(fileName) User Defaults
    public extension UserDefaults {
        static var savedUserThemeStyle: \(style) {
            guard
                let saved = standard.value(forKey: "\(style)") as? Int,
                let style = \(style)(rawValue: saved) else {
                return .unknown
            }
            return style
        }
        static func saveUserThemeStyle(_ style: \(style)) {
            standard.setValue(style.rawValue, forKey: "\(style)")
            standard.synchronize()
        }
    }
    
    """
        
        template +=
    """
    
    
    /// \(styleEnum.name)
    public struct \(fileName): BWTheme {
        public static let shared = \(fileName)()
        
        public var currentStyle: \(style) {
            return UserDefaults.savedUserThemeStyle
        }
        public func updateStyle(with newStyle: \(style)) {
            guard currentStyle != newStyle else {
                return
            }
            UserDefaults.saveUserThemeStyle(newStyle)
            NotificationCenter.default.post(name: .\(style)DidChange, object: nil)
        }
    }
    
    /// \(style)s
    public typealias \(style) = \(fileName).Style
    public extension \(fileName) {
        enum Style: Int, BWThemeStyle {
            case unknown = -1
            case \(styleEnum.cases.joined(separator: "\n        case "))
        }
    }
    
    
    """
        return template
    }
    
    static func uiViewExtensionFile() -> String {
"""
import UIKit

enum ThemeTool {
    static func objc_swizzleSelector1(
        _ sel1: Selector,
        inClass1 cls1: AnyClass,
        withSelector2 sel2: Selector,
        inClass2 cls2: AnyClass
    ) {
        guard let m1 = class_getInstanceMethod(cls1, sel1),
              let m2 = class_getInstanceMethod(cls2, sel2) else {
            return
        }
        let isM1Added = class_addMethod(cls1, sel1, method_getImplementation(m2), method_getTypeEncoding(m2))
        if isM1Added {
            class_replaceMethod(cls2, sel2, method_getImplementation(m1), method_getTypeEncoding(m1))
        }
        else {
            method_exchangeImplementations(m1, m2)
        }
    }
}

extension UIView {
    private enum ASKey {
        static var tColorKey = "tColorKey"
        static var tColorValue = "tColorValue"
    }
    
    @objc class func themeableSetups() {
        ThemeTool.objc_swizzleSelector1(
            #selector(UIView.init(frame:)),
            inClass1: Self.self,
            withSelector2: #selector(self_init(frame:)),
            inClass2: Self.self
        )
        ThemeTool.objc_swizzleSelector1(
            #selector(awakeFromNib),
            inClass1: Self.self,
            withSelector2: #selector(self_awakeFromNib),
            inClass2: Self.self
        )
    }
    
    @IBInspectable
    var tColorKey: String {
        get {
            return objc_getAssociatedObject(self, &ASKey.tColorKey) as? String ?? ""
        }
        set{
            objc_setAssociatedObject(self, &ASKey.tColorKey, newValue, .OBJC_ASSOCIATION_COPY)
        }
    }
    
    @IBInspectable
    var tColorValue: String {
        get {
            return objc_getAssociatedObject(self, &ASKey.tColorValue) as? String ?? ""
        }
        set{
            objc_setAssociatedObject(self, &ASKey.tColorValue, newValue, .OBJC_ASSOCIATION_COPY)
        }
    }
    
    private var getXibThemedColor: UIColor? {
        let colorValue = tColorValue
        guard colorValue.contains(".") else {
            return nil
        }
        let components = colorValue.components(separatedBy: ".")
        guard components.count == 2 else {
            return nil
        }
        return #Theme#ColorStore.shared.findColor(
            forStyle: #Theme#.shared.currentStyle,
            atScope: components[0],
            colorName: components[1]
        )
    }
    
    @objc func self_init(frame: CGRect) {
        self_init(frame: frame)

        guard let _ = self as? #Theme#Adaptable else {
            return
        }
        themeStyleDidChange()
    }
    
    /// when self is `awakeFromNib` or `themeStyleDidChange`,
    /// then update setting the `theme configured semantic color`
    @objc func self_awakeFromNib() {
        self_awakeFromNib()
        
        setupThemeable()
    }
    
    func setupThemeable(){
        updateXibThemedColor()
        
        let view = self as? #Theme#Adaptable
        guard view != nil || getXibThemedColor != nil else {
            return
        }
        NotificationCenter.default.addObserver(self, selector: #selector(themeStyleDidChange), name: .#Theme#StyleDidChange, object: nil)
    }
    
    @objc func updateXibThemedColor() {
        guard
            !tColorKey.isEmpty,
            !tColorValue.isEmpty,
            let color = getXibThemedColor else {
            return
        }
        setValue(color, forKeyPath: tColorKey)
    }
    
    @objc func themeStyleDidChange() {
        updateXibThemedColor()
        
        (self as? #Theme#Adaptable)?.updateThemeStyleIfNeeded()
    }
    
}

"""
    }
    
    static func uiViewControllerExtensionFile() -> String {
"""
import UIKit

extension UIViewController {
    static func themeableSetups() {
        ThemeTool.objc_swizzleSelector1(
            #selector(viewDidLoad),
            inClass1: Self.self,
            withSelector2: #selector(self_viewDidLoad),
            inClass2: Self.self
        )
    }

    /// when self is `awakeFromNib` or `themeStyleDidChange`,
    /// then update setting the `theme configured semantic color`
    @objc private func self_viewDidLoad() {
        self_viewDidLoad()
        
        guard let _ = self as? #Theme#Adaptable else {
            return
        }
        
        (self as? #Theme#Adaptable)?.updateThemeStyleIfNeeded()
        
        NotificationCenter.default.addObserver(self, selector: #selector(themeStyleDidChange), name: .#Theme#StyleDidChange, object: nil)
    }
    
    @objc private func themeStyleDidChange() {
        (self as? #Theme#Adaptable)?.updateThemeStyleIfNeeded()
    }
}

"""
    }
    
    static func updateSwiftExtensionFiles(themeName: String, projectDir: String) {
        func rewriteFile(_ fileName: String, content: String) {
            let string = content.replacingOccurrences(of: "#Theme#", with: themeName)
            let savePath = themeGeneratesFolder(projectDir: projectDir) + "/\(fileName)"
            let success = saveText(string, toFile: savePath)
            print("rewriteFile: ", savePath, success)
        }
        rewriteFile("UIView+Themeable.swift", content: uiViewExtensionFile())
        rewriteFile("UIViewController+Themeable.swift", content: uiViewControllerExtensionFile())
    }
    
    static func swiftFileTemplate(forThemeColorEnum colorEnum: Enum) -> String {
        let subs = colorEnum.subs
        var template =
    """
    import UIKit
    import BWThemeKit

    /// \(colorEnum.name)Storage
    public typealias \(colorEnum.name)Store = \(colorEnum.name).Store
    public extension \(colorEnum.name) {
        struct Store: BWThemeColorStoreable {
            public typealias StoreEnum = \(colorEnum.name)
            
            public var colorStore: BWThemeColorStore? = Self.loadStore()
            public static let shared = Store()
            public static let filePath: URL? = Bundle.main.url(forResource: "\(themeColorsSaveName(configureName: colorEnum.name))", withExtension: nil)
        }
    }
    
    """
        template += """
    
    /// \(colorEnum.name)
    public enum \(colorEnum.name) {
    }
    
    """
        
        for sub in subs {
            template +=
    """
    
    /// \(sub.name) Colors
    public extension \(colorEnum.name) {
        enum \(sub.name): Int, BWThemeColor, \(colorEnum.name)Quickable {
            case \(sub.cases.joined(separator: "\n        case "))
        }
    }
    
    """
        }
        return template
    }
}

struct ThemeConfigure: Decodable {
    var name: String
    var styles: [String]
    var colors: ThemeConfigureColor
}

struct ThemeConfigureColor: Decodable {
    var name: String
    var files: [String]
}

// loads
extension Generator {
    static func loadThemeConfigure(projectDir: String) -> ThemeConfigure? {
        let path = themeConfigureFolder(projectDir: projectDir)
        let result = decodeJSON(fileAtPath: path, toObjectType: ThemeConfigure.self)
        guard
            case .success(let configure) = result
        else {
            print("loadThemeConfigure Error")
            return nil
        }
        return configure
    }
    
    static func loadThemeColors(
        fileName: String,
        projectDir: String
    ) -> [ThemeColorStoreScope]? {
        let path = themeColorFilePath(fileName: fileName, projectDir: projectDir)
        let result = decodeJSON(fileAtPath: path, toObjectType: [ThemeColorStoreScope].self)
        guard
            case .success(let colors) = result
        else {
            print("loadThemeColors Error")
            return nil
        }
        return colors
    }
    
    static func themeConfigureFolder(projectDir: String) -> String {
        return themeFolder(projectDir: projectDir) + "/Configure.json"
    }
    static func themeFolder(projectDir: String) -> String {
        return projectDir + "/BWTheme"
    }
    static func themeColorsFolder(projectDir: String) -> String {
        return themeFolder(projectDir: projectDir)
        + "/Colors"
    }
    static func themeGeneratesFolder(projectDir: String) -> String {
        let path = themeFolder(projectDir: projectDir)
        + "/Generates"
        if !FileManager.default.fileExists(atPath: path) {
            try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false)
        }
        return path
    }
    static func themeColorFilePath(
        fileName: String,
        projectDir: String
    ) -> String {
        return themeColorsFolder(projectDir: projectDir)
        + "/\(fileName).json"
    }
    static func themeSwiftFileSavePath(
        fileName: String,
        projectDir: String
    ) -> String {
        return themeGeneratesFolder(projectDir: projectDir)
        + "/\(fileName).swift"
    }
    static func themeColorsSaveName(configureName: String) -> String {
        let name = !configureName.hasSuffix("s") ? configureName+"s" : configureName
        return "\(name).json"
    }
    static func themeColorsSavePath(
        configureName: String,
        projectDir: String
    ) -> String {
        return themeGeneratesFolder(projectDir: projectDir)
        + "/" + themeColorsSaveName(configureName: configureName)
    }
}


// color stores
struct ThemeColorStore: Decodable, Encodable {
    var name: String
    var styles: [ThemeColorStoreStyle]
}

struct ThemeColorStoreStyle: Decodable, Encodable {
    var name: String
    var scopes: [ThemeColorStoreScope]
}

struct ThemeColorStoreScope: Decodable, Encodable {
    var name: String
    var colors: [ThemeColorStoreColor]
}

struct ThemeColorStoreColor: Decodable, Encodable {
    var name: String
    var rgba: String
}


